#include <slothttpd/stage/proxy_fcgi.hpp>
#include <slothttpd/http/status.hpp>
#include <slothttpd/fcgi/fcgi.hpp>
#include <slothttpd/http/header/responce.hpp>
#include <slothttpd/logger.hpp>
#include <slothttpd/http/http.hpp>

#include <cstddef>
#include <cstdio>

#include <fcntl.h>

#include <iostream>

#include <boost/bind.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/range/algorithm/copy.hpp>

namespace slothttpd {
namespace stage {

namespace asio = boost::asio;
namespace placeholder = boost::asio::placeholders;

proxy_fcgi::proxy_fcgi(connection_ptr conn, config::fcgi_location &conf) :
    ref_counted     (conn),
    request_        (nullptr),
    conf_           (conf),
    out_buffer_     (data_buffer::from_pool(conn->pool, conf.fcgi_output_buffer_size)),
    stdout_buffers_ (conf.fcgi_stdout_buffers.size, conf.fcgi_stdout_buffers.nmax),
    stderr_buffers_ (conf.fcgi_stderr_buffers.size, conf.fcgi_stderr_buffers.nmax),
    stdout_state_   (stdout_parse_headers),
    stdout_sent_    (false),
    write_pending_  (false),
    params_state_   (conf.params_range()),
    stdin_state_    (stdin_params)
{
    // should never throw
    request_ = conf.app->begin_request(
                &stdout_buffers_, &stderr_buffers_,
                boost::bind(&proxy_fcgi::next_buffer, proxy_fcgi_ptr(this)),
                boost::bind(&proxy_fcgi::handle_event, proxy_fcgi_ptr(this), _1, _2));
}

char_range_t proxy_fcgi::next_buffer(proxy_fcgi_ptr f)
{
    f->out_buffer_.reset();
    
    switch (f->stdin_state_) {
    case stdin_params:
        if (!f->params_state_.empty()) {
            while (f->append_params())
                ;
            
            return f->out_buffer_.unprocessed();
        } else {
            f->c->pool.free(f->out_buffer_.data);
            
            auto content = f->c->request.content;
            
            if (content != nullptr) {
                f->stdin_state_ = stdin_content;
                f->stdin_left_  = content->length;
            } else {
                f->stdin_state_ = stdin_done;
            }
        }
        break;
        
    case stdin_content:
        if (f->stdin_left_ == 0) {
            f->stdin_state_ = stdin_done;
            break;
        } else {
            auto content = f->c->request.content;
            auto data_end = content->data + content->length;
            
            std::size_t sending = std::min(f->stdin_left_, fcgi::max_content_length);
            
            char *begin = data_end - f->stdin_left_;
            char_range_t retval(begin, begin + sending);
            
            f->stdin_left_ -= sending;
            
            return retval;
        }
        break;
        
    case stdin_done:
        break;
    }
    
    return char_range_t(nullptr, nullptr);
}

void proxy_fcgi::handle_event(proxy_fcgi_ptr f, char *record, const boost::system::error_code &err)
{
    if (!err) {
        fcgi::header_view v(record);
        
        switch (v.type()) {
        case fcgi::type::stdout:
            if (f->stdout_buffers_.get_data().empty()) {
                f->stdout_buffers_.disable();
            }
            
            switch (f->stdout_state_) {
            case stdout_parse_headers:
                switch (f->stdout_parser_.parse(f->stdout_buffers_, 
                                                boost::bind(&proxy_fcgi::header_found, f.get(), _1, _2))) {
                case parser::parse_again:
                    break;
                    
                case parser::parse_ok:
                    f->stdout_state_ = stdout_content;
                    //f->c->responce.push_back(asio::buffer(http::headers::response::connection_close));
                    f->c->responce.push_back(asio::buffer(http::headers::response::transfer_encoding_chunked));
                    f->c->responce.push_back(asio::buffer(http::headers::response::connection_keep_alive));
                    for (auto &b : f->conf_.header_buffers) {
                        f->c->responce.push_back(b);
                    }
                    f->c->responce.push_back(asio::buffer(http::crlf));
                    
                    f->c->post_write_responce(boost::bind(&proxy_fcgi::handle_write_chunk, f, placeholder::error));
                    f->write_pending_ = true;
                    break;
                    
                case parser::parse_error:
                    break;
                }
                break;
            
            case stdout_content:
                f->post_write_chunk();
                break;
            }
            break;
            
        case fcgi::type::stderr:
            break;
            
        case fcgi::type::end_request: {
                fcgi::end_request_view e(record);
                
                /*
                logger() << "end_request " << f->request_.id() << ": app_status = " << e.app_status()
                          << ", protocol status = " << int(e.protocol_status()) << '\n';
                */
                // disconnect BEFORE starting new write operation
                f->request_.disconnect();
                f->post_write_chunk();
                
                if (!f->stderr_buffers_.get_data().empty()) {
                    auto &log = logger();
                    bool last_lf = false;
                    for (auto r = f->stderr_buffers_.get_data(); !r.empty(); r = f->stderr_buffers_.get_data()) {
                        log.write(r.begin(), r.size());
                        last_lf = (r.back() == '\n');
                        f->stderr_buffers_.consume(r.size());
                    }
                    if (!last_lf) {
                        log << '\n';
                    }
                }
            }
            break;
        }
    } else {
        if (!f->stdout_sent_) {
            f->c->send_stock_response(http::status::internal_server_error);
        } else {
            f->c->close();
        }
    }
}

void proxy_fcgi::handle_write_response(proxy_fcgi_ptr f, const boost::system::error_code &err)
{
    f->stdout_buffers_.switch_dont_save();
    
    handle_write_chunk(f, err);
}

void proxy_fcgi::post_write_chunk()
{
    if (write_pending_) {
        return;
    }
    
    char_range_t r  = stdout_buffers_.get_data();
    auto buf        = asio::buffer(r.begin(), r.size());
    
    if (r.size() > 0) {
        auto n = std::snprintf(chunk_size_.data(), chunk_size_.size(), "%x\r\n", asio::buffer_size(buf));
        
        three_buffers[0] = asio::buffer(chunk_size_.data(), n);
        three_buffers[1] = buf;
        three_buffers[2] = asio::buffer(http::crlf);
        
        stdout_sending_ = asio::buffer_size(buf);
        
        boost::asio::async_write(c->socket, 
                                 three_buffers,
                                 asio::transfer_all(),
                                 boost::bind(&proxy_fcgi::handle_write_chunk,
                                             proxy_fcgi_ptr(this),
                                             boost::asio::placeholders::error));
        write_pending_ = true;
    } else {
        if (!request_.is_active())
            post_write_final_chunk();
    }
}

void proxy_fcgi::handle_write_chunk(proxy_fcgi_ptr f, const boost::system::error_code &err)
{
    f->write_pending_ = false;
    
    if (!err) {
        if (f->stdout_sent_) {
            f->stdout_buffers_.consume(f->stdout_sending_);
        }
        
        f->stdout_sent_ = true;
        f->post_write_chunk();
    } else {
        
    }
}

void proxy_fcgi::post_write_final_chunk()
{
    // TODO: handle trailer properly. how ? I dont know! Find out!
    auto n = std::snprintf(chunk_size_.data(), chunk_size_.size(), "0\r\n\r\n");
    
    boost::asio::async_write(c->socket, 
                             asio::buffer(chunk_size_.data(), n),
                             asio::transfer_all(),
                             boost::bind(&proxy_fcgi::handle_write_final_chunck,
                                         proxy_fcgi_ptr(this),
                                         boost::asio::placeholders::error));
    write_pending_ = true;
}

void proxy_fcgi::handle_write_final_chunck(proxy_fcgi_ptr f, const boost::system::error_code &err)
{
    f->write_pending_ = false;
}

void proxy_fcgi::store_len(char **const b__, std::uint32_t len)
{
    std::uint8_t **b = reinterpret_cast<std::uint8_t **>(b__);
    if (len < 128) {
        **b = std::uint8_t(len);
        (*b) += 1;
    } else {
        fcgi::header_view::store_uint32(len, *b);
        (*b) += 4;
    }
}

bool proxy_fcgi::append_params()
{
    if (params_state_.empty())
        return false;
    
    const config::fcgi_location::params_type &param = params_state_.front();
    
    const std::string &name = param.name;
    std::size_t value_size = param.value_formatter.size(c->request);
    
    int name_length_size = (name.size() < 128) ? 1 : 4;
    int value_length_size = (value_size < 128) ? 1 : 4;
    
    std::size_t kv_pair_len = name.size() + name_length_size + 
                              value_size  + value_length_size;
    
    auto free_space = out_buffer_.free_space();
    if (kv_pair_len < std::size_t(free_space.size())) {
        char *p = free_space.begin();
        
        store_len(&p, name.size());
        store_len(&p, value_size);
        
        p = boost::range::copy(name, p);
        
        free_space = char_range_t(p, free_space.end());
        param.value_formatter.place(free_space, c->request);
        
        out_buffer_.unprocessed_last = free_space.begin();
        
        params_state_.advance_begin(1);
        
        return true;
    }
    
    return false;
}

void proxy_fcgi::header_found(char_range_t name, char_range_t value)
{
    if (boost::algorithm::iequals(name, "Status")) {
        c->responce.status_line(value);
        //logger() << "Status line: " << value << '\n';
    } else {
        c->responce.push_back(asio::buffer(name.begin(), name.size()));
        c->responce.push_back(asio::buffer(http::semicolon));
        c->responce.push_back(asio::buffer(value.begin(), value.size()));
        c->responce.push_back(asio::buffer(http::crlf));
        
        //logger() << "fcgi: " << name << " " << value << '\n';
    }
}

}
}
